//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 1995-2000, The Hypersonic SQL Group.
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the Hypersonic SQL Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE HYPERSONIC SQL GROUP,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * on behalf of the Hypersonic SQL Group.
 *
 *
 * For work added by the HSQL Development Group:
 *
 * Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using EffiProzDB.Lib;
using EffiProzDB.ScriptIO;

namespace EffiProzDB.Persist
{
    // fredt@users 20020215 - patch 1.7.0 by fredt
    // to move operations on the database.properties files to new
    // class HsqlDatabaseProperties
    // fredt@users 20020220 - patch 488200 by xclayl@users - throw exception
    // throw addded to all methods relying on file io
    // fredt@users 20020221 - patch 513005 by sqlbob@users (RMP)
    // fredt@users 20020405 - patch 1.7.0 by fredt - no change _in db location
    // because important information about the database is now stored _in the
    // *.properties file, all database files should be _in the same folder as the
    // *.properties file
    // tony_lai@users 20020820 - export hsqldb.log_size to .properties file
    // tony_lai@users 20020820 - changes to shutdown compact to save memory
    // fredt@users 20020910 - patch 1.7.1 by Nitin Chauhan - code improvements
    // fredt@users 20021208 - ongoing revamp
    // fredt@users 20021212 - do not rewrite the *.backup file if the *.data
    // file has not been updated _in the current session.
    // boucherb@users 20030510 - patch 1.7.2 consolidated all periodic database
    // tasks _in one timed task queue
    // fredt@users - 20050102 patch 1.8.0 - refactoring and clearer separation of concerns

    /**
     *  This class is responsible for managing the database files. An HSQLDB database
     *  consists of a .properties file, a .script file (contains an SQL script),
     *  a .data file (contains data of cached tables) a .backup file
     *  and a .log file.<p>
     *  When using TEXT tables, a data source for each table is also present.<p>
     *
     *  Notes on OpenOffice.org integration.
     *
     *  A Storage API is used when HSQLDB is integrated into OpenOffice.org. All
     *  file operations on the 4 main files are performed by OOo, which integrates
     *  the contents of these files into its database file. The script format is
     *  always TEXT _in this case.
     *
     * Extensively rewritten and extended _in successive versions of HSQLDB.
     *
     * @author Thomas Mueller (Hypersonic SQL Group)
     * @author fredt@users.
     * @version 1.8.0
     * @since Hypersonic SQL
     */
    public class Log
    {

        private HsqlDatabaseProperties properties;
        private string fileName;
        private Database _database;
        private HSQLFileAccess fa;
        private ScriptWriterBase dbLogWriter;
        private string scriptFileName;
        private string logFileName;
        private bool filesReadOnly;
        private long maxLogSize;
        private int writeDelay;
        private int scriptFormat;
        private DataFileCache cache;

        public Log(Database db)
        {

            _database = db;
            fa = db.getFileAccess();
            fileName = db.getPath();
            properties = db.getProperties();
        }

          public   void initParams() {

                // Allows the user to set log size _in the properties file.
                int logMegas = properties.getIntegerProperty(
                    HsqlDatabaseProperties.hsqldb_log_size, 0);

                maxLogSize = logMegas * 1024 * 1024;
                scriptFormat = properties.getIntegerProperty(
                    HsqlDatabaseProperties.hsqldb_script_format,
                    ScriptWriterBase.SCRIPT_TEXT_170);
                writeDelay     = properties.getDefaultWriteDelay();
                filesReadOnly  = _database.isFilesReadOnly();
                scriptFileName = fileName + ".script";
                logFileName    = fileName + ".log";
            }

            /**
             * When opening a database, the hsqldb.compatible_version property is
             * used to determine if this version of the engine is equal to or greater
             * than the earliest version of the engine capable of opening that
             * database.<p>
             *
             * @throws  HsqlException
             */
          public   void open()   {

                initParams();

                int state = properties.getDBModified();

                switch (state) {

                    case HsqlDatabaseProperties.FILES_MODIFIED :
                        deleteNewAndOldFiles();
                        restoreBackup();
                        processScript();
                        processDataFile();
                        processLog();
                        close(false);

                        if (cache != null) {
                            cache.open(filesReadOnly);
                        }

                        reopenAllTextCaches();
                        break;

                    case HsqlDatabaseProperties.FILES_NEW :
                        try {
                            deleteBackup();
                            backupData();
                            renameNewBackup();
                            renameNewScript();
                            deleteLog();
                            properties.setDBModified(
                                HsqlDatabaseProperties.FILES_NOT_MODIFIED);
                        } catch (IOException e) {
                            _database.logger.appLog.logContext(e, null);
                        }
                        break;
                    // continue as non-modified files
                    case HsqlDatabaseProperties.FILES_NOT_MODIFIED :

                        /**
                         * if startup is after a SHUTDOWN SCRIPT and there are CACHED
                         * or TEXT tables, perform a checkpoint so that the .script
                         * file no longer contains CACHED or TEXT table rows.
                         */
                        processScript();

                        if (isAnyCacheModified()) {
                            properties.setDBModified(
                                HsqlDatabaseProperties.FILES_MODIFIED);
                            close(false);

                            if (cache != null) {
                                cache.open(filesReadOnly);
                            }

                            reopenAllTextCaches();
                        }
                        break;
                }

                openLog();

                if (!filesReadOnly) {
                    properties.setDBModified(HsqlDatabaseProperties.FILES_MODIFIED);
                }
            }

            /**
             * Close all the database files. If script argument is true, no .data
             * or .backup file will remain and the .script file will contain all the
             * data of the cached tables as well as memory tables.
             *
             * This is not used for filesReadOnly databases which use shutdown.
             */
           public  void close(bool script)   {

                closeLog();
                deleteNewAndOldFiles();
                writeScript(script);
                closeAllTextCaches(script);

                if (cache != null) {
                    cache.close(true);
                }

                properties.setProperty(HsqlDatabaseProperties.db_version,
                                       HsqlDatabaseProperties.THIS_VERSION);
                properties.setProperty(
                    HsqlDatabaseProperties.hsqldb_compatible_version,
                    HsqlDatabaseProperties.FIRST_COMPATIBLE_VERSION);

                // set this one last to save the props
                properties.setDBModified(HsqlDatabaseProperties.FILES_NEW);
                deleteLog();

                if (script) {
                    deleteBackup();
                    deleteData();
                } else {
                    try {
                        backupData();
                        renameNewBackup();
                    } catch (IOException) {}
                }

                renameNewScript();
                properties.setProperty(HsqlDatabaseProperties.hsqldb_cache_version,
                                       HsqlDatabaseProperties.THIS_CACHE_VERSION);
                properties.setDBModified(HsqlDatabaseProperties.FILES_NOT_MODIFIED);
            }

            /**
             * Fast counterpart to close(). Does not perform a checkpoint or a backup
             * of the .data file.
             */
            public void shutdown()   {

                synchLog();

                if (cache != null) {
                    cache.close(false);
                }

                closeAllTextCaches(false);
                closeLog();
            }

            /**
             * Deletes the leftovers from any previous unfinished operations.
             */
           public  void deleteNewAndOldFiles() {

                fa.RemoveElement(fileName + ".data" + ".old");
                fa.RemoveElement(fileName + ".data" + ".new");
                fa.RemoveElement(fileName + ".backup" + ".new");
                fa.RemoveElement(scriptFileName + ".new");
            }

          public   void deleteBackup() {
                fa.RemoveElement(fileName + ".backup");
            }

           public  void deleteData() {
                fa.RemoveElement(fileName + ".data");
            }

           public  void backupData()   {

                if (fa.IsStreamElement(fileName + ".data")) {
                    ZipUnzipFile.compressFile(fileName + ".data",
                                              fileName + ".backup.new",
                                              _database.getFileAccess());
                }
            }

            public void renameNewBackup() {

                if (fa.IsStreamElement(fileName + ".backup.new")) {
                    fa.RenameElement(fileName + ".backup.new", fileName + ".backup");
                }
            }

           public  void renameNewScript() {

                if (fa.IsStreamElement(scriptFileName + ".new")) {
                    fa.RenameElement(scriptFileName + ".new", scriptFileName);
                }
            }

           public  void deleteNewScript() {
                fa.RemoveElement(scriptFileName + ".new");
            }

            void deleteNewBackup() {
                fa.RemoveElement(scriptFileName + ".backup.new");
            }

           public  void deleteLog() {
                fa.RemoveElement(logFileName);
            }

            /**
             * Checks all the caches and returns true if the modified flag is set for any
             */
           public  bool isAnyCacheModified() {

                if (cache != null && cache.isFileModified()) {
                    return true;
                }

                return isAnyTextCacheModified();
            }

            /**
             * Performs checkpoint including pre and post operations. Returns to the
             * same state as before the checkpoint.
             */
          public   void checkpoint(bool defrag)   {

                if (filesReadOnly) {
                    return;
                }

                _database.logger.appLog.logContext(SimpleLog.LOG_NORMAL, "start");
                deleteNewAndOldFiles();

                if (cache != null) {
                    if (forceDefrag()) {
                        defrag = true;
                    }

                    if (defrag) {
                        try {
                            cache.defrag();
                        } catch (Exception ) {}
                    } else {
                        cache.close(true);

                        try {
                            cache.backupFile();
                        } catch (IOException ) {
                            deleteNewBackup();
                            cache.open(false);

                            return;
                        }

                        cache.open(false);
                    }
                }

                writeScript(false);
                properties.setDBModified(HsqlDatabaseProperties.FILES_NEW);
                closeLog();
                deleteLog();
                renameNewScript();
                renameNewBackup();
                properties.setDBModified(HsqlDatabaseProperties.FILES_MODIFIED);

                if (dbLogWriter == null) {
                    return;
                }

                openLog();

                Session[] sessions = _database.sessionManager.getAllSessions();

                try {
                    for (int i = 0; i < sessions.Length; i++) {
                        Session session = sessions[i];

                        if (session.isAutoCommit() == false) {
                            dbLogWriter.writeLogStatement(
                                session, session.getAutoCommitStatement());
                        }
                    }
                } catch (IOException ) {
                    throw Trace.error(Trace.FILE_IO_ERROR, logFileName);
                }

                _database.logger.appLog.logContext(SimpleLog.LOG_NORMAL, "end");
            }

            /**
             * Returns true if lost space is above the threshold
             */
            public bool forceDefrag() {

                long megas = properties.getIntegerProperty(
                    HsqlDatabaseProperties.hsqldb_defrag_limit, 200);
                long defraglimit = megas * 1024 * 1024;
                long lostSize    = cache.freeBlocks.getLostBlocksSize();

                return lostSize > defraglimit;
            }

            /**
             *
             */
            public bool hasCache() {
                return cache != null;
            }

            /**
             * Responsible for creating the cache instance.
             */
           public  DataFileCache getCache()   {

                if (cache == null) {
                    cache = new DataFileCache(_database, fileName);

                    cache.open(filesReadOnly);
                }

                return cache;
            }

          public   int getLogSize() {
                return (int) (maxLogSize / (1024 * 1024));
            }

           public  void setLogSize(int megas) {

                properties.setProperty(HsqlDatabaseProperties.hsqldb_log_size,
                                       (megas.ToString()));

                maxLogSize = megas * 1024 * 1024;
            }

          public   int getScriptType() {
                return scriptFormat;
            }

            /**
             * Changing the script format results _in a checkpoint, with the .script
             * file written _in the new format.
             */
         public    void setScriptType(int type)   {

                // OOo related code
                if (_database.isStoredFileAccess()) {
                    return;
                }

                // end OOo
                bool needsCheckpoint = scriptFormat != type;

                scriptFormat = type;

                properties.setProperty(HsqlDatabaseProperties.hsqldb_script_format,
                                       (scriptFormat.ToString()));

                if (needsCheckpoint) {
                    _database.logger._needsCheckpoint = true;
                }
            }

            /**
             * Write delay specifies the frequency of FileDescriptor.sync() calls.
             */
           public  int getWriteDelay() {
                return writeDelay;
            }

            public void setWriteDelay(int delay) {

                writeDelay = delay;

                if (dbLogWriter != null) {
                    synchLog();
                    dbLogWriter.setWriteDelay(delay);
                }
            }

            /**
             * Various writeXXX() methods are used for logging statements.
             */
            public void writeStatement(Session session, string s)   {

                if (s == null || s.Length  == 0) {
                    return;
                }

                try {
                    dbLogWriter.writeLogStatement(session, s);
                } catch (IOException ) {
                    throw Trace.error(Trace.FILE_IO_ERROR, logFileName);
                }

                if (maxLogSize > 0 && dbLogWriter.size() > maxLogSize) {
                    _database.logger._needsCheckpoint = true;
                }
            }

           public  void writeInsertStatement(Session session, Table t,
                                      Object[] row)   {

                try {
                    dbLogWriter.writeInsertStatement(session, t, row);
                } catch (IOException) {
                    throw Trace.error(Trace.FILE_IO_ERROR, logFileName);
                }

                if (maxLogSize > 0 && dbLogWriter.size() > maxLogSize) {
                    _database.logger._needsCheckpoint = true;
                }
            }

            public void writeDeleteStatement(Session session, Table t,
                                      Object[] row)   {

                try {
                    dbLogWriter.writeDeleteStatement(session, t, row);
                } catch (IOException) {
                    throw Trace.error(Trace.FILE_IO_ERROR, logFileName);
                }

                if (maxLogSize > 0 && dbLogWriter.size() > maxLogSize) {
                    _database.logger._needsCheckpoint = true;
                }
            }

        public void writeSequenceStatement(Session session,
                                    NumberSequence s)
        {

            try
            {
                dbLogWriter.writeSequenceStatement(session, s);
            }
            catch (IOException)
            {
                throw Trace.error(Trace.FILE_IO_ERROR, logFileName);
            }

            if (maxLogSize > 0 && dbLogWriter.size() > maxLogSize)
            {
                _database.logger._needsCheckpoint = true;
            }
        }

           public  void writeCommitStatement(Session session)   {

                try {
                    dbLogWriter.writeCommitStatement(session);
                } catch (IOException ) {
                    throw Trace.error(Trace.FILE_IO_ERROR, logFileName);
                }

                if (maxLogSize > 0 && dbLogWriter.size() > maxLogSize) {
                    _database.logger._needsCheckpoint = true;
                }
            }

           public  void synchLog() {

                if (dbLogWriter != null) {
                    dbLogWriter.sync();
                }
            }

            /**
             * Wrappers for openning-starting / stoping-closing the log file and
             * writer.
             */
            private void openLog()   {

                if (filesReadOnly) {
                    return;
                }

                try {
                    dbLogWriter = ScriptWriterBase.newScriptWriter(_database,
                            logFileName, false, false,
                            ScriptWriterBase.SCRIPT_TEXT_170);

                    dbLogWriter.setWriteDelay(writeDelay);
                    dbLogWriter.start();
                } catch (Exception ) {
                    throw Trace.error(Trace.FILE_IO_ERROR, logFileName);
                }
            }

            private  void closeLog()   {

                lock (this)
                {
                    if (dbLogWriter != null)
                    {
                        dbLogWriter.close();
                    }
                }
            }

            /**
             * Write the .script file as .script.new.
             */
            private void writeScript(bool full)   {

                deleteNewScript();

                //fredt - to do - flag for chache set index
                ScriptWriterBase scw = ScriptWriterBase.newScriptWriter(_database,
                    scriptFileName + ".new", full, true, scriptFormat);

                scw.writeAll();
                scw.close();
            }

            /**
             * Performs all the commands _in the .script file.
             */
            private void processScript()   {

                ScriptReaderBase scr = null;

                try {
                    if ( fa.IsStreamElement(scriptFileName)) {
                        scr = ScriptReaderBase.newScriptReader(_database,
                                                               scriptFileName,
                                                               scriptFormat);

                        scr.readAll(_database.sessionManager.getSysSession(null, true));
                        scr.close();
                    }
                } catch (Exception e) {
                    if (scr != null) {
                        scr.close();

                        if (cache != null) {
                            cache.close(false);
                        }

                        closeAllTextCaches(false);
                    }

                    _database.logger.appLog.logContext(e, null);

                    if (e is HsqlException) {
                        throw (HsqlException) e;
                    } else if (e is IOException) {
                        throw Trace.error(Trace.FILE_IO_ERROR, e.ToString());
                    }else {
                        throw Trace.error(Trace.GENERAL_ERROR, e.ToString());
                    }
                }
            }

            /**
             * Defrag large data files when the sum of .log and .data files is large.
             */
            private void processDataFile()   {

                // OOo related code
                if (_database.isStoredFileAccess()) {
                    return;
                }

                // OOo end
                if (cache == null || filesReadOnly
                        || !fa.IsStreamElement(logFileName)) {
                    return;
                }

                FileInfo file = new FileInfo(logFileName);
                long logLength  = file.Length ;
                long dataLength = cache.getFileFreePos();

                if (logLength + dataLength > cache.maxDataFileSize) {
                    _database.logger._needsCheckpoint = true;
                }
            }

            /**
             * Performs all the commands _in the .log file.
             */
            private void processLog()   {

                if ( fa.IsStreamElement(logFileName)) {
                    ScriptRunner.runScript(_database, logFileName,
                                           ScriptWriterBase.SCRIPT_TEXT_170);
                }
            }

            /**
             * Restores a compressed backup or the .data file.
             */
            private void restoreBackup()   {

                // _in case data file cannot be deleted, reset it
                DataFileCache.deleteOrResetFreePos(_database, fileName + ".data");

                try {
                    ZipUnzipFile.decompressFile(fileName + ".backup",
                                                fileName + ".data",
                                                _database.getFileAccess());
                } catch (Exception e) {
                    throw Trace.error(Trace.FILE_IO_ERROR, Trace.Message_Pair,
                                      new Object[] {
                        fileName + ".backup", e.ToString()
                    });
                }
            }

        // fredt@users 20020221 - patch 513005 by sqlbob@users (RMP) - text tables
            private Dictionary<HsqlNameManager.HsqlName, TextCache> textCacheList = new Dictionary<HsqlNameManager.HsqlName, TextCache>();

            public DataFileCache openTextCache(Table table, string source,
                                        bool readOnlyData,
                                        bool reversed)   {

                closeTextCache(table);

                if (!properties.isPropertyTrue(
                        HsqlDatabaseProperties.textdb_allow_full_path)) {
                    if (source.IndexOf("..") != -1) {
                        throw (Trace.error(Trace.ACCESS_IS_DENIED, source));
                    }

                    string path = 
                        new FileInfo( _database.getPath() + ".properties").Directory.FullName;

                    if (path != null) {
                        source = path + Path.DirectorySeparatorChar + source;
                    }
                }

                TextCache c;          

                if (reversed) {
                    c = new TextCache(table, source);
                } else {
                    c = new TextCache(table, source);
                }

                c.open(readOnlyData || filesReadOnly);
                textCacheList.Add(table.getName(), c);

                return c;
            }

            public void closeTextCache(Table table)   {

                TextCache c = null;

                if (textCacheList.TryGetValue(table.getName(),out c))
                {
                    textCacheList.Remove(table.getName());
                    c.close(true);
                }
            }

            private void closeAllTextCaches(bool compact)   {

                foreach (var c in textCacheList.Values) {
                    if (compact) {
                        c.purge();
                    } else {
                        c.close(true);
                    }
                }
            }

            private void reopenAllTextCaches()   {              

                foreach (var c in textCacheList.Values)
                {
                    c.reopen();
                }
            }

            private bool isAnyTextCacheModified() {            

                foreach (var c in textCacheList.Values)
                {
                    if (c.isFileModified()) {
                        return true;
                    }
                }

                return false;
            }
    }
}